package com.zzh.lib.recorder.encoder;

import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaFormat;
import android.media.MediaMuxer;
import android.media.MediaRecorder;
import android.util.Log;

import java.io.IOException;
import java.nio.ByteBuffer;

public class MediaMuxerCaptureWrapper {
    private static final String TAG = "MediaMuxerWrapper";

    private final MediaMuxer mediaMuxer;
    private int encoderCount, startedCount;
    private boolean isStarted;
    private MediaEncoder videoEncoder, audioEncoder;
    private long preventAudioPresentationTimeUs = -1;
    private int audioTrackIndex = -1;

    /**
     * Constructor
     */
    public MediaMuxerCaptureWrapper(final String filePath) throws IOException {
        mediaMuxer = new MediaMuxer(filePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
        encoderCount = startedCount = 0;
        isStarted = false;

    }

    public void prepare() throws IOException {
        if (videoEncoder != null) {
            videoEncoder.prepare();
        }
        if (audioEncoder != null) {
            audioEncoder.prepare();
        }
    }

    public void startRecording() {
        if (videoEncoder != null) {
            videoEncoder.startRecording();
        }
        if (audioEncoder != null) {
            audioEncoder.startRecording();
        }
    }

    public void stopRecording() {
        if (videoEncoder != null) {
            videoEncoder.stopRecording();
        }
        videoEncoder = null;
        if (audioEncoder != null) {
            audioEncoder.stopRecording();
        }
        audioEncoder = null;
    }

    public synchronized boolean isStarted() {
        return isStarted;
    }

//**********************************************************************
//**********************************************************************

    /**
     * assign encoder to this calss. this is called from encoder.
     *
     * @param encoder instance of MediaVideoEncoder or MediaAudioEncoder
     */
    void addEncoder(final MediaEncoder encoder) {
        if (encoder instanceof MediaVideoEncoder) {
            if (videoEncoder != null)
                throw new IllegalArgumentException("Video encoder already added.");
            videoEncoder = encoder;
        } else if (encoder instanceof MediaAudioEncoder) {
            if (audioEncoder != null)
                throw new IllegalArgumentException("Video encoder already added.");
            audioEncoder = encoder;
        } else
            throw new IllegalArgumentException("unsupported encoder");
        encoderCount = (videoEncoder != null ? 1 : 0) + (audioEncoder != null ? 1 : 0);
    }

    /**
     * request start recording from encoder
     *
     * @return true when muxer is ready to write
     */
    synchronized boolean start() {
        Log.v(TAG, "start:");
        startedCount++;
        if ((encoderCount > 0) && (startedCount == encoderCount)) {
            mediaMuxer.start();
            isStarted = true;
            notifyAll();
            Log.v(TAG, "MediaMuxer started:");
        }
        return isStarted;
    }

    /**
     * request stop recording from encoder when encoder received EOS
     */
    /*package*/
    synchronized void stop() {
        Log.v(TAG, "stop:startedCount=" + startedCount);
        startedCount--;
        if ((encoderCount > 0) && (startedCount <= 0)) {
            mediaMuxer.stop();
            mediaMuxer.release();
            isStarted = false;
            Log.v(TAG, "MediaMuxer stopped:");
        }
    }

    /**
     * assign encoder to muxer
     *
     * @param format
     * @return minus value indicate error
     */
    synchronized int addTrack(final MediaFormat format) {
        if (isStarted) {
            throw new IllegalStateException("muxer already started");
        }

        final int trackIx = mediaMuxer.addTrack(format);
        Log.i(TAG, "addTrack:trackNum=" + encoderCount + ",trackIx=" + trackIx + ",format=" + format);

        String mime = format.getString(MediaFormat.KEY_MIME);
        if (!mime.startsWith("video/")) {
            audioTrackIndex = trackIx;
        }
        return trackIx;
    }

    /**
     * write encoded data to muxer
     *
     * @param trackIndex
     * @param byteBuf
     * @param bufferInfo
     */
    /*package*/
    synchronized void writeSampleData(final int trackIndex, final ByteBuffer byteBuf, final MediaCodec.BufferInfo bufferInfo) {
        //bufferInfo.presentationTimeUs
        if (startedCount <= 0) return;

        if (audioTrackIndex == trackIndex) {
            if (preventAudioPresentationTimeUs < bufferInfo.presentationTimeUs) {
                mediaMuxer.writeSampleData(trackIndex, byteBuf, bufferInfo);
                preventAudioPresentationTimeUs = bufferInfo.presentationTimeUs;
            }
        } else {
            mediaMuxer.writeSampleData(trackIndex, byteBuf, bufferInfo);
        }
    }

    public static class MediaAudioEncoder extends MediaEncoder {
        private static final String TAG = "MediaAudioEncoder";

        private static final String MIME_TYPE = "audio/mp4a-latm";
        private static final int SAMPLE_RATE = 44100;    // 44.1[KHz] is only setting guaranteed to be available on all devices.
        private static final int BIT_RATE = 64000;
        public static final int SAMPLES_PER_FRAME = 1024;    // AAC, bytes/frame/channel
        public static final int FRAMES_PER_BUFFER = 25;    // AAC, frame/buffer/sec

        private AudioThread audioThread = null;

        public MediaAudioEncoder(final MediaMuxerCaptureWrapper muxer, final MediaEncoderListener listener) {
            super(muxer, listener);
        }

        @Override
        protected void prepare() throws IOException {
            Log.v(TAG, "prepare:");
            trackIndex = -1;
            muxerStarted = isEOS = false;
            // prepare MediaCodec for AAC encoding of audio data from inernal mic.
            final MediaCodecInfo audioCodecInfo = selectAudioCodec(MIME_TYPE);
            if (audioCodecInfo == null) {
                Log.e(TAG, "Unable to find an appropriate codec for " + MIME_TYPE);
                return;
            }
            Log.i(TAG, "selected codec: " + audioCodecInfo.getName());

            final MediaFormat audioFormat = MediaFormat.createAudioFormat(MIME_TYPE, SAMPLE_RATE, 1);
            audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
            audioFormat.setInteger(MediaFormat.KEY_CHANNEL_MASK, AudioFormat.CHANNEL_IN_MONO);
            audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
            audioFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
            Log.i(TAG, "format: " + audioFormat);
            mediaCodec = MediaCodec.createEncoderByType(MIME_TYPE);
            mediaCodec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            mediaCodec.start();
            Log.i(TAG, "prepare finishing");
            if (listener != null) {
                try {
                    listener.onPrepared(this);
                } catch (final Exception e) {
                    Log.e(TAG, "prepare:", e);
                }
            }
        }

        @Override
        protected void startRecording() {
            super.startRecording();
            // create and execute audio capturing thread using internal mic
            if (audioThread == null) {
                audioThread = new AudioThread();
                audioThread.start();
            }
        }

        @Override
        protected void release() {
            audioThread = null;
            super.release();
        }

        private static final int[] AUDIO_SOURCES = new int[]{
                MediaRecorder.AudioSource.MIC,
                MediaRecorder.AudioSource.DEFAULT,
                MediaRecorder.AudioSource.CAMCORDER,
                MediaRecorder.AudioSource.VOICE_COMMUNICATION,
                MediaRecorder.AudioSource.VOICE_RECOGNITION,
        };

        /**
         * Thread to capture audio data from internal mic as uncompressed 16bit PCM data
         * and write them to the MediaCodec encoder
         */
        private class AudioThread extends Thread {
            @Override
            public void run() {
                android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
                try {
                    final int min_buffer_size = AudioRecord.getMinBufferSize(
                            SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO,
                            AudioFormat.ENCODING_PCM_16BIT);
                    int buffer_size = SAMPLES_PER_FRAME * FRAMES_PER_BUFFER;
                    if (buffer_size < min_buffer_size)
                        buffer_size = ((min_buffer_size / SAMPLES_PER_FRAME) + 1) * SAMPLES_PER_FRAME * 2;

                    AudioRecord audioRecord = null;
                    for (final int source : AUDIO_SOURCES) {
                        try {
                            audioRecord = new AudioRecord(
                                    source, SAMPLE_RATE,
                                    AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, buffer_size);
                            if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED)
                                audioRecord = null;
                        } catch (final Exception e) {
                            audioRecord = null;
                        }
                        if (audioRecord != null) break;
                    }
                    if (audioRecord != null) {
                        try {
                            if (isCapturing) {
                                Log.v(TAG, "AudioThread:start audio recording");
                                final ByteBuffer buf = ByteBuffer.allocateDirect(SAMPLES_PER_FRAME);
                                int readBytes;
                                audioRecord.startRecording();
                                try {
                                    while (isCapturing && !requestStop && !isEOS) {
                                        // read audio data from internal mic
                                        buf.clear();
                                        readBytes = audioRecord.read(buf, SAMPLES_PER_FRAME);
                                        if (readBytes > 0) {
                                            // set audio data to encoder
                                            buf.position(readBytes);
                                            buf.flip();
                                            encode(buf, readBytes, getPTSUs());
                                            frameAvailableSoon();
                                        }
                                    }
                                    frameAvailableSoon();
                                } finally {
                                    audioRecord.stop();
                                }
                            }
                        } finally {
                            audioRecord.release();
                        }
                    } else {
                        Log.e(TAG, "failed to initialize AudioRecord");
                    }
                } catch (final Exception e) {
                    Log.e(TAG, "AudioThread#run", e);
                }
                Log.v(TAG, "AudioThread:finished");
            }
        }

        /**
         * select the first codec that match a specific MIME type
         *
         * @param mimeType
         * @return
         */
        private static final MediaCodecInfo selectAudioCodec(final String mimeType) {
            Log.v(TAG, "selectAudioCodec:");

            MediaCodecInfo result = null;
            // get the list of available codecs
            final int numCodecs = MediaCodecList.getCodecCount();
            LOOP:
            for (int i = 0; i < numCodecs; i++) {
                final MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
                if (!codecInfo.isEncoder()) {    // skipp decoder
                    continue;
                }
                final String[] types = codecInfo.getSupportedTypes();
                for (int j = 0; j < types.length; j++) {
                    Log.i(TAG, "supportedType:" + codecInfo.getName() + ",MIME=" + types[j]);
                    if (types[j].equalsIgnoreCase(mimeType)) {
                        if (result == null) {
                            result = codecInfo;
                            break LOOP;
                        }
                    }
                }
            }
            return result;
        }

    }
}

